<?php

namespace Shared\EventListener;

use Doctrine\ORM\Event\OnFlushEventArgs,
	Doctrine\Common\EventArgs,
	Doctrine\Common\EventSubscriber;
use DateTime,
	stdClass;

/**
 * 
 * @author Peter Yordanov <p.jordanov@object.bg>
 * @copyright (c) 2014, ASPECT.BG Ltd.
 */
class EntityModification implements EventSubscriber {

	private static $entity;

	/**
	 *
	 * @var \Shared\Entity\Admin\User
	 */
	private $user;

	/**
	 *
	 * @var \Doctrine\ORM\EntityManager
	 */
	private $em;

	/**
	 *
	 * @var \Shared\Entity\Admin\Audit\ThreadLog
	 */
	private $thread;

	/**
	 *
	 * @var type 
	 */
	private $pendingEntities = [];
	private $entityLogToEntityMapping = [];
	private $isDeleted = false;

	public function getUser() {
		return $this->user;
	}

	public function setUser(\Shared\Entity\Admin\User $user) {
		$this->user = $user;
	}

	public static function getEntity() {
		return self::$entity;
	}

	public static function setEntity($entity = null) {
		self::$entity = $entity;
	}

	/**
	 * 
	 * @return \Shared\Entity\Admin\Audit\ThreadLog
	 */
	public function getThread() {
		if (!$this->thread) {
			$this->thread = new \Shared\Entity\Admin\Audit\ThreadLog;
			$this->thread->setDatetime(new \DateTime);
			$this->thread->setUser($this->user);
			$this->thread->setContextEntityFqcn($this->getRealFqcn(self::$entity));
			$this->thread->setContextEntityId($this->getEntityId(self::$entity));
		}
		return $this->thread;
	}

	private function isLoggable($entity) {
		return $entity instanceof AuditableInterface;
	}

	public function postFlush(\Doctrine\ORM\Event\PostFlushEventArgs $eventArgs) {
		if (!self::$entity) {
			return false;
		}
		$uow = $this->em->getUnitOfWork();

		$thread = $this->getThread();
		if (!$thread->getId()) {
			return false;
		}

		$qb = $this->em->createQueryBuilder();

		$qb->update(get_class($thread), 'thread')
			->where('thread = :thread')
			->setParameter('thread', $thread);

		$qb->set('thread.action', ':action');

		if (!$thread->getContextEntityId()) {
			$qb->setParameter('action', \Shared\Entity\Admin\Audit\EntityLog::ACTION_INSERT);
			$qb->set('thread.contextEntityId', ':entityId')
				->setParameter('entityId', $this->getEntityId(self::$entity));
		} elseif ($this->isDeleted) {
			$qb->setParameter('action', \Shared\Entity\Admin\Audit\EntityLog::ACTION_DELETE);
		} else {
			$qb->setParameter('action', \Shared\Entity\Admin\Audit\EntityLog::ACTION_UPDATE);
		}
		$qb->getQuery()->execute();

		foreach ($this->pendingEntities as $entityLog) {
			/* @var $entityLog \Shared\Entity\Admin\Audit\EntityLog */
			if ($entityLog->getChangedEntityId()) {
				continue;
			}
			$entity = $this->entityLogToEntityMapping[spl_object_hash($entityLog)];
			$qb = $this->em->createQueryBuilder();

			$qb->update('Shared\Entity\Admin\Audit\EntityLog', 'log')
				->set('log.changedEntityId', ':entityId')
				->where('log = :log')
				->setParameter('entityId', $this->getEntityId($entity))
				->setParameter('log', $entityLog)
				->getQuery()
				->execute();
		}
		$this->pendingEntities = [];
		$this->entityLogToEntityMapping = [];
		$this->isDeleted = false;
	}

	public function onFlush(OnFlushEventArgs $eventArgs) {
		if (!self::$entity) {
			return false;
		}
		$this->em = $eventArgs->getEntityManager();
		$uow = $this->em->getUnitOfWork();

		$thread = $this->getThread();

		foreach ($uow->getScheduledEntityInsertions() AS $entity) {
			$this->processEntity($entity, \Shared\Entity\Admin\Audit\EntityLog::ACTION_INSERT);
		}

		foreach ($uow->getScheduledEntityUpdates() AS $entity) {
			$this->processEntity($entity, \Shared\Entity\Admin\Audit\EntityLog::ACTION_UPDATE);
		}

		foreach ($uow->getScheduledEntityDeletions() AS $entity) {
			if ($entity == self::$entity) {
				$this->isDeleted = true;
			}
			$this->processEntity($entity, \Shared\Entity\Admin\Audit\EntityLog::ACTION_DELETE);
		}

		if ($this->pendingEntities) {
			$this->em->persist($thread);
			$uow->computeChangeSet($this->em->getClassMetadata(get_class($thread)), $thread);
		}

		foreach ($this->pendingEntities as $entityLog) {
			$this->em->persist($entityLog);
			$uow->computeChangeSet($this->em->getClassMetadata(get_class($entityLog)), $entityLog);
		}
	}

	/**
	 * 
	 * @param \Shared\EventListener\AuditableInterface $entity
	 * @param type $action
	 * @return \Shared\Entity\Admin\Audit\EntityLog
	 */
	private function processEntity($entity, $action) {
		if (!$this->isLoggable($entity) and $entity != self::$entity) {
			return false;
		}

		$entityLog = new \Shared\Entity\Admin\Audit\EntityLog;
		$entityLog->setChangedEntityFqcn($this->getRealFqcn($entity));
		$entityLog->setChangedEntityId($this->getEntityId($entity));
		$entityLog->setAction($action);
		$entityLog->setThread($this->getThread());

		switch ($action) {
			case \Shared\Entity\Admin\Audit\EntityLog::ACTION_UPDATE:
				$changes = new stdClass;
				$changes->diff = $this->getEntityChanges($entity);

				if (!$changes->diff) {
					return false;
				}

				$entityLog->setChanges($changes);
				break;
			case \Shared\Entity\Admin\Audit\EntityLog::ACTION_INSERT:
				break;
		}

		$this->pendingEntities[spl_object_hash($entity)] = $entityLog;
		$this->entityLogToEntityMapping[spl_object_hash($entityLog)] = $entity;
	}

	/**
	 * 
	 * @param array $fieldMappingMetaData
	 * @param array $change
	 */
	private function getFieldChanges(array $fieldMappingMetaData, array $change) {
		$type = $fieldMappingMetaData['type'];
		$checkDate = false;
		if ($type == 'date' or $type == 'datetime' or $type == 'time') {
			list ($old, $new) = $change;
			if ($old and $new and $old instanceof DateTime and $new instanceof DateTime) {
				$checkDate = true;
			}
		}
		if (!$checkDate) {
			if (trim($change[0]) === trim($change[1])) {
				return false;
			}
			return $change;
		}
		switch ($type) {
			case 'date':
				if ($checkDate and $old->format('Ymd') !== $new->format('Ymd')) {
					$change = array(
						$old->format('Y-m-d'),
						$new->format('Y-m-d')
					);
				} else {
					return false;
				}
				break;
			case 'datetime':
				if ($checkDate and $old->format('YmdHis') !== $new->format('YmdHis')) {
					$change = array(
						$old->format('Y-m-d H:i:s'),
						$new->format('Y-m-d H:i:s')
					);
				} else {
					return false;
				}
				break;
			case 'time':
				if ($checkDate and $old->format('His') !== $new->format('His')) {
					$change = array(
						$old->format('H:i:s'),
						$new->format('H:i:s')
					);
				} else {
					return false;
				}
				break;
		}

		return $change;
	}

	/**
	 * 
	 * @param array $changes
	 * @return array
	 */
	private function getAssociationChanges(array $changes) {
		if ($changes[0] and is_object($changes[0])) {
			$changes[0] = $this->getEntityId($changes[0]);
		}
		if ($changes[1] and is_object($changes[1])) {
			$changes[1] = $this->getEntityId($changes[1]);
		}

		return $changes;
	}

	/**
	 * 
	 * @param object $entity
	 * @return stdClass
	 */
	private function getEntityChanges($entity) {
		$changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($entity);

		$metaData = $this->em->getClassMetadata($this->getRealFqcn($entity));

		$return = [];

		foreach ($changeSet as $propertyName => $change) {
			$differenceObject = new stdClass;
			if (array_key_exists($propertyName, $metaData->fieldMappings)) {
				$fieldMapping = $metaData->getFieldMapping($propertyName);
				$change = $this->getFieldChanges($fieldMapping, $change);
				if (false === $change) {
					continue;
				}
				list($differenceObject->before, $differenceObject->after) = $change;
			} elseif (array_key_exists($propertyName, $metaData->associationMappings)) {
				$change = $this->getAssociationChanges($change);
				if (false === $change) {
					continue;
				}
				list($differenceObject->before, $differenceObject->after) = $change;
			}
			if ($differenceObject->before != $differenceObject->after) {
				$return[$propertyName] = $differenceObject;
			}
		}

		return $return;
	}

	private function getEntityId($object) {
		$id = $this->em->getClassMetadata(get_class($object))
			->getSingleIdReflectionProperty()
			->getValue($object);

		if (is_object($id)) {
			$id = $this->getEntityId($id);
		}

		return $id;
	}

	private function getRealFqcn($entity) {
		return \Shared\EntityUtils::getEntityFQCN($entity);
	}

	public function getSubscribedEvents() {
		return array('onFlush', 'postFlush');
	}

}
